using System;
using System.Collections.Generic;
using System.Reflection;
using System.Data;
using System.Data.Common;

namespace NyMPH.Persistence
{
    /// <summary>
    /// Provides data mapping methods that can be used for any object and datarow.
    /// Adapted from this person's code: http://www.codeproject.com/Members/karthika-rani (thanks!)
    /// </summary>
    public class DataMapping
    {
        #region Mapping Methods (DataRow/DataReader to Object)
        /// <summary>
        /// Copies values from the source into the properties of the target.
        /// </summary>
        /// <param name="SourceRow">A datarow containing the source values.</param>
        /// <param name="Target">A referenced object with properties to be set from the datarow.</param>
        /// <param name="IgnoreList">A list of property names to ignore. These properties will not be set on the target object.</param>
        /// <param name="SuppressExceptions">If <see langword="true" />, any exceptions will be supressed.</param>
        /// <remarks>
        /// The column names in the datarow must match the property names on the target object. Target properties may not be readonly or indexed.
        /// </remarks>
        public static void Map(DataRow SourceRow, Object TargetObject, bool SuppressExceptions, params string[] IgnoreList)
        {
            //create list of properties to exclude, keep it null if nothing was passed in
            List<string> ignore = (IgnoreList == null) ? null : ignore = new List<string>(IgnoreList);
            
            //Loop thru the Columns in the DataRow
            for (int i = 0; i <= SourceRow.ItemArray.Length - 1; i++)
            {
                //Get the column name 
                string _propertyName = SourceRow.Table.Columns[i].ColumnName;

                if (ignore == null || (ignore != null && !ignore.Contains(_propertyName)))
                {
                    try
                    {//set the value to the object
                        SetPropertyValue(TargetObject, _propertyName, SourceRow[_propertyName]);
                    }
                    catch (Exception ex)
                    {
                        if (!SuppressExceptions)
                            throw new Exception(string.Format("Error while assigning value to the property : {0}", _propertyName), ex);
                    }
                }
            }
        }

        /// <summary>
        /// Copies values from the source into the properties of the target.
        /// </summary>
        /// <param name="SourceDataReader">A DbDataReader containing the source values.</param>
        /// <param name="Target">A referenced object with properties to be set from the DbDataReader.</param>
        /// <param name="IgnoreList">A list of property names to ignore. These properties will not be set on the target object.</param>
        /// <param name="SuppressExceptions">If <see langword="true" />, any exceptions will be supressed.</param>
        /// <remarks>
        /// The column names in the DbDataReader must match the property names on the target object. Target properties may not be readonly or indexed.
        /// </remarks>
        public static void Map(DbDataReader SourceDataReader, ref Object Target, bool SuppressExceptions, params string[] IgnoreList)
        {
            //create list of properties to exclude, keep it null if nothing was passed in
            List<string> ignore = (IgnoreList == null) ? null : ignore = new List<string>(IgnoreList);

            //Loops till DataReader has rows in case of Multi Select Satatement 
            while (SourceDataReader.HasRows)
            {
                while (SourceDataReader.Read())
                {
                    //Loops thru each Column in the DataReader 
                    for (int i = 0; i <= SourceDataReader.FieldCount - 1; i++)
                    {
                        //gets the Column Name in the reader 
                        string _propertyName = SourceDataReader.GetName(i);

                        if (ignore == null || (ignore != null && !ignore.Contains(_propertyName)))
                        {
                            try
                            {
                                //Sets the value from reader to Object
                                SetPropertyValue(Target, _propertyName, SourceDataReader[_propertyName]);

                            }
                            catch (Exception ex)
                            {
                                if (!SuppressExceptions)
                                {
                                    throw new Exception(string.Format("Error while assigning value to the property : {0}", _propertyName), ex);
                                }
                            }//end try
                        }//end if
                    }//end for
                }

                SourceDataReader.NextResult();
            }//while loop ends here
        }

        #endregion

        /// <summary>
        /// Sets an object's property with the specified value, coercing that value to the appropriate type if possible.
        /// </summary>
        /// <param name="target">Object containing the property to set.</param>
        /// <param name="propertyName">Name of the property to set.</param>
        /// <param name="value">Value to set into the property.</param>
        public static void SetPropertyValue(object Target, string PropertyName, object Value)
        {
            //Get the property of the Target object(Class) using the Column Name from the Datarow/DataReader
           PropertyInfo propertyInfo = Target.GetType().GetProperty(PropertyName);

            //Get the Type (datatype)of the Property of the target class
            Type pType = GetPropertyType(propertyInfo.PropertyType);

            //Get the Type (datatype) of the Value from the Datarow/DataReader
            Type vType = GetPropertyType(Value.GetType());

            //For null values in the Datarow/DataReader set the property value as DBnull 
            if (Value == null)
            {
                propertyInfo.SetValue(Target, DBNull.Value, null);
            }
            else if (Value.Equals(DBNull.Value))
            {
                //get default values for certain data types
                //TODO: fix this up a bit!
                if (pType.Equals(typeof(DateTime)))
                    propertyInfo.SetValue(Target, DateTime.MinValue, null);

                else if (pType.Equals(typeof(Double)) || pType.Equals(typeof(Int32)) )
                {
                    propertyInfo.SetValue(Target, 0, null);
                }

                else if (pType.Equals(typeof(Byte[])))
                {
                    propertyInfo.SetValue(Target, new byte[0], null);
                }
                
            }
            else
            {
                //For not null values in the Datarow/DataReader
                if (pType.Equals(vType))
                {
                    // types match, just copy value
                    propertyInfo.SetValue(Target, Value, null);
                }
                else
                {
                    // types don't match, try to coerce
                    if (pType.Equals(typeof(Guid)))
                        propertyInfo.SetValue(Target, new Guid(Value.ToString()), null);
                    else if (pType.IsEnum && vType.Equals(typeof(string)))
                        propertyInfo.SetValue(Target, Enum.Parse(pType, Value.ToString()), null);
                    else
                        propertyInfo.SetValue(Target, Convert.ChangeType(Value, pType), null);
                }
            }
        }

        /// <summary>
        /// Returns a property's type, dealing with Nullable(Of T) if necessary.
        /// </summary>
        /// <param name="propertyType">Type of the property as returned by reflection.</param>
        private static Type GetPropertyType(Type T)
        {
            if (T.IsGenericType && (T.GetGenericTypeDefinition() == typeof(Nullable<>)))
            {
                return Nullable.GetUnderlyingType(T);
            }

            return T;
        }
    }
}
